# coding=utf-8
from math import log

import esphome.config_validation as cv
import esphome.codegen as cg
from esphome.components import sensor
from esphome.const import UNIT_CELSIUS, ICON_THERMOMETER, CONF_SENSOR, CONF_TEMPERATURE, \
    CONF_VALUE, CONF_CALIBRATION, CONF_ID

ntc_ns = cg.esphome_ns.namespace('ntc')
NTC = ntc_ns.class_('NTC', cg.Component, sensor.Sensor)

CONF_B_CONSTANT = 'b_constant'
CONF_REFERENCE_TEMPERATURE = 'reference_temperature'
CONF_REFERENCE_RESISTANCE = 'reference_resistance'
CONF_A = 'a'
CONF_B = 'b'
CONF_C = 'c'
ZERO_POINT = 273.15


def validate_calibration_parameter(value):
    if isinstance(value, dict):
        return cv.Schema({
            cv.Required(CONF_TEMPERATURE): cv.float_,
            cv.Required(CONF_VALUE): cv.float_,
        })(value)

    value = cv.string(value)
    parts = value.split('->')
    if len(parts) != 2:
        raise cv.Invalid(u"Calibration parameter must be of form 3000 -> 23°C")
    voltage = cv.resistance(parts[0].strip())
    temperature = cv.temperature(parts[1].strip())
    return validate_calibration_parameter({
        CONF_TEMPERATURE: temperature,
        CONF_VALUE: voltage,
    })


def calc_steinhart_hart(value):
    r1 = value[0][CONF_VALUE]
    r2 = value[1][CONF_VALUE]
    r3 = value[2][CONF_VALUE]
    t1 = value[0][CONF_TEMPERATURE] + ZERO_POINT
    t2 = value[1][CONF_TEMPERATURE] + ZERO_POINT
    t3 = value[2][CONF_TEMPERATURE] + ZERO_POINT

    l1 = log(r1)
    l2 = log(r2)
    l3 = log(r3)

    y1 = 1/t1
    y2 = 1/t2
    y3 = 1/t3

    g2 = (y2-y1)/(l2-l1)
    g3 = (y3-y1)/(l3-l1)

    c = (g3-g2)/(l3-l2) * 1/(l1+l2+l3)
    b = g2 - c*(l1*l1 + l1*l2 + l2*l2)
    a = y1 - (b + l1*l1*c) * l1
    return a, b, c


def calc_b(value):
    beta = value[CONF_B_CONSTANT]
    t0 = value[CONF_REFERENCE_TEMPERATURE] + ZERO_POINT
    r0 = value[CONF_REFERENCE_RESISTANCE]

    a = (1/t0) - (1/beta) * log(r0)
    b = 1/beta
    c = 0

    return a, b, c


def process_calibration(value):
    if isinstance(value, dict):
        value = cv.Schema({
            cv.Required(CONF_B_CONSTANT): cv.float_,
            cv.Required(CONF_REFERENCE_TEMPERATURE): cv.temperature,
            cv.Required(CONF_REFERENCE_RESISTANCE): cv.resistance,
        })(value)
        a, b, c = calc_b(value)
    elif isinstance(value, list):
        if len(value) != 3:
            raise cv.Invalid("Steinhart–Hart Calibration must consist of exactly three values")
        value = cv.Schema([validate_calibration_parameter])(value)
        a, b, c = calc_steinhart_hart(value)
    else:
        raise cv.Invalid("Calibration parameter accepts either a list for steinhart-hart "
                         "calibration, or mapping for b-constant calibration, "
                         "not {}".format(type(value)))

    return {
        CONF_A: a,
        CONF_B: b,
        CONF_C: c,
    }


CONFIG_SCHEMA = sensor.sensor_schema(UNIT_CELSIUS, ICON_THERMOMETER, 1).extend({
    cv.GenerateID(): cv.declare_id(NTC),
    cv.Required(CONF_SENSOR): cv.use_id(sensor.Sensor),
    cv.Required(CONF_CALIBRATION): process_calibration,
}).extend(cv.COMPONENT_SCHEMA)


def to_code(config):
    var = cg.new_Pvariable(config[CONF_ID])
    yield cg.register_component(var, config)
    yield sensor.register_sensor(var, config)

    sens = yield cg.get_variable(config[CONF_SENSOR])
    cg.add(var.set_sensor(sens))
    calib = config[CONF_CALIBRATION]
    cg.add(var.set_a(calib[CONF_A]))
    cg.add(var.set_b(calib[CONF_B]))
    cg.add(var.set_c(calib[CONF_C]))
